Key

scroll

Key Blog

  • Key ホームページ>
  • ブログ>
  • [ue5] 利用unreal insights進行優化:透過非同步加載(asyncload)提升遊戲性能
  • [UE5] Unreal Insightsを活用した最適化:非同期ロードでゲームパフォーマンスを向上

    @kiikey4(Key Zhao)

    [UE5] Unreal Insightsを活用した最適化:非同期ロードでゲームパフォーマンスを向上

    最終更新日 2024年10月20日

    投稿日 2024年10月19日

    1

    概要

    この記事では、Unreal Insight を使用して最適化の負荷調査を行う流れと、ゲーム内でアセットをLoadSynchronous()で同期的に読み込む際に発生するヒッチを防ぐための対策として、C++による非同期ロード(裏読み)の実装方法を紹介します。Blueprint での非同期ロードについては、下記の参考リンクを参照してください。

    環境

    • Rider 2024.2.6
    • Unreal Engine 5.4
    • Windows 11 Pro

    参考資料

    本編

    Unreal Insight を使った最適化の負荷調査

    Unreal Insights って? Unreal Insights は、Unreal Engine でのパフォーマンスやメモリの使用状況を分析するためのツールです。公式ドキュメントはこちら: : https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine

    使用方法
    詳しい使用方法については [UE5] UnrealInsights を使ってみよう を参照してください。

    最適化調査を行う際には、通常の PIE(Play In Editor)モードではなく、ターゲットプラットフォームのパッケージ版を使用するのが理想です。PIE モードでは事前に読み込まれたキャッシュや背景での動作が影響し、正確な負荷測定が難しくなるためです。

    今回調査のは PIE よりパッケージ版の環境に近い Standalone Game で調査を行います。

    Standalone Game の起動方法

    下図のように、UE エディターから Standalone Game を起動します。

    how_to_launch_standalone_game_nbf9t2

    ゲームプレイ中にヒッチ(カクつき)が発生した場合、次のコマンドを活用しましょう。

    stat_unitgraph_qvvmlg

    stat unitgraph

    「stat UnitGraph」コマンドは、ゲームの処理負荷をグラフ形式で可視化するツールです。これにより、ヒッチが発生するタイミングでグラフに大きなスパイクが表示され、問題の箇所を明確に特定できます。

    多くの人がよく使用する「stat Unit」や「stat fps」とは異なり、「stat UnitGraph」では短時間のヒッチもしっかりグラフに残るため、見逃すことが少ないです。

    実行後は、以下のようなグラフが表示されます。左下には負荷のグラフ、右上には具体的な数値が表示されます。60fps を目指す場合は、Frame 時間を16.6ms以下に保つことが理想です。

    stat_unitgraph_explanation_mdv1fe

    次に、Unreal Insight を使って実際に計測を行います。

    trace_start_aq3qbs

    trace.starttrace.stop

    Unreal Insight で計測を始めるには、trace.startコマンドを実行します。計測が完了したら、trace.stopで終了します。

    処理負荷計測結果

    下図の動画のようにヒッチの発生が確認しました。

    hitch_graph_nl0bb9

    重い処理が実行されると、グラフ上に大きな変化が現れます。

    hitch_frame_gucxrj

    Game: 58.26ms ゲームスレッドに 58.26ms もの時間がかかっているのがわかります。

    次に、Unreal Insight の計測結果も確認してみましょう。

    Trace を開く方法 open_trace_ws9cha

    Trace データを開くと、以下の結果が表示されます。 HitchhUnrealInsight_wlbcuz

    この結果で注目すべきは、緑のバーで示されている

    LoadObject (154.7ms) - /Game/Main/InGame/VFX/Niagara/NS_DizzyStar.NS_DizzyStar
    

    です。ここから、Niagara エフェクトのローディング処理が大きな負荷をかけていることがわかります。

    特に、Niagara の初回ロード時や初回スポーン時に、シェーダーのコンパイルが原因でヒッチが発生します。

    今回は初回ロード時にヒッチが発生する原因について説明しますが、もし初回スポーン時にヒッチが発生する場合は、事前に(暗転などの演出中に)一度見えない場所でスポーンさせることで解決することが可能です。

    パッケージ版の場合、インストール後の最初の 1 回だけこのヒッチが発生します。 UE では UE 起動後初回のみ発生します。

    ヒッチを再現したい場合は、Unreal Engine を再起動するか、パッケージを削除して再インストールする必要があります。

    これが原因の Niagara エフェクトです。

    DizzyNiagaraEffect_x8gioz

    C++側のコードを見ると、ローディング処理は次のようになっています。

    PlayerCharacter.h
    1public: 2 //... 3 UPROPERTY(EditAnywhere, BlueprintReadWrite) 4 TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset; 5 //...
    PlayerCharacter.cpp
    1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.LoadSynchronous(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}

    エフェクトをスポーンする直前にLoadSynchronous()でエフェクトのアセットを同期にロードしていることが、ヒッチの原因となっています。

    LoadSynchronous()(同期ロード)はロード完了するまで待つ(他の処理を止める)ということです。これによりプレイヤーはヒッチが感じます。

    UE 公式では非同期ロードを推奨します。

    参考動画: Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022

    動画の 22:40 から 28:15 までは非同期ロード関連になります

    27:33 には Blueprint での非同期ロード(AsyncLoad)の実装方法です

    TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset エフェクトのアセットはプレイヤーに持たれています。 DizzyEffectAsset_wsa9ro

    調査結果:原因はエフェクトスポーンする直前のLoadSynchoronous()です。

    非同期ロード(AsyncLoad)を実装する

    同期ロードによるヒッチを解消するために、ゲーム開始時にアセットを事前に非同期ロード(裏読み)します。

    非同期ロードには時間がかかる場合があるため、事前に非同期ロードの時間を確保しないと、アセットのスポーンが間に合わないことがあります。

    これから、C++で非同期ロードの関数を作成します。

    PlayerCharacter.h
    1protected: 2 void OnDizzyEffectLoaded(); 3 void LoadDizzyEffectAsset();
    PlayerCharacter.cpp
    1void APlayerCharacter::BeginPlay() 2{ 3 Super::BeginPlay(); 4 LoadDizzyEffectAsset(); 5} 6 7void APlayerCharacter::LoadDizzyEffectAsset() 8{ 9 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset requeset load")); 10 UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(DizzyEffectAsset.ToSoftObjectPath(), 11 FStreamableDelegate::CreateUObject( 12 this, &APlayerCharacter::OnDizzyEffectLoaded)); 13} 14 15void APlayerCharacter::OnDizzyEffectLoaded() 16{ 17 UE_LOG(LogTemp, Log, TEXT("DizzyEffectLoaded")); 18 19 if (IsValid(DizzyEffectAsset.Get())) 20 { 21 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset is valid")); 22 } 23 else 24 { 25 UE_LOG(LogTemp, Error, TEXT("DizzyEffectAsset is null, Function name: %s"), *FString(__FUNCTION__)); 26 } 27}

    次に、アセットの同期ロードをGet()に置換

    PlayerCharacter.cpp
    1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.Get(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}

    これで非同期ロードの実装が完了しました。 次に、Standalone ゲーム内に確認してみましょう。

    ログでDizzyEffectAsset is validと表示されば、アセットの非同期ロード(AsyncLoad)が成功したことが確認できます。

    結果

    「stat UnitGraph」コマンドを使用した結果、グラフ上のスパイクがなくなり、ヒッチが解消されました。

    まとめ

    調査の流れ

    • ヒッチの検出:ゲーム中に発生したヒッチを「stat unitgraph」コマンドを使ってグラフ化し、重い処理が発生する箇所を特定。
    • Unreal Insight での計測:ヒッチ発生時の詳細な負荷を調査するため、trace.start および trace.stop コマンドを使用してトレースデータを収集し、負荷の発生源を確認。

    問題の特定

    • エフェクトのアセット(Niagara エフェクト)が同期ロードLoadSynchronous()によって読み込まれていたため、ロードが完了するまで他の処理が止まり、ヒッチが発生していることが判明。

    非同期ロードの実装

    • ヒッチを回避するために、TSoftObjectPtr を使用して、エフェクトをゲーム開始時に非同期でロードする処理を追加。これにより、プレイヤーがエフェクトを使用する前にアセットが読み込まれ、ヒッチを防ぎます。

    結果

    非同期ロードの実装により、ヒッチが解消され、ゲームプレイがスムーズになったことが確認されました。 このように、非同期ロードを用いることでゲームパフォーマンスを向上させ、プレイヤーの体験を改善できました。

    1

    コメント

    この記事にコメントはありません。

    感想をコメントしましょう